{
 "cells": [
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# Build trustworthy LLM agents & applications for production with Instructor + Burr\n",
    "\n",
    "The challenge with large language models (LLMs) is handling the 5% of the time they say crazy things. Being able to debug why an output is bad and having tools for fixing are critical requirements for making LLM features / agents trustworthy and available to users.\n",
    "\n",
    "In this notebook, you'll learn how `instructor` can make LLM reliability produce structured outputs, and `burr` helps you introspect and debug your application."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Instructor 101\n",
    "Instructor is a tool to help you prompt LLM and constraint its outputs. First, you specify the desired output using a **model** with typed fields and textual descriptions; you can think of it as a template that the LLM will fill. This greatly improves the reliability of the content and format of generated text.\n",
    "\n",
    "To introduce Instructor, we'll write code to generate a social media post from the transcript of a YouTube video. \n",
    "\n",
    "> This post on the Instructor blog is also a great introduction: [Analyzing Youtube Transcripts with Instructor](https://python.useinstructor.com/blog/2024/07/11/youtube-transcripts/)."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### 1. Define the `response_model`\n",
    "\n",
    "Instructor uses [Pydantic](https://docs.pydantic.dev/latest/) to create the response model. A model needs to inherit the `BaseModel` class and we use the `Field()` object to give a textual description.\n",
    "\n",
    "- `Field()` objects allow to specify constraints to the generated output. For instance, we want \"1 to 3 concepts\" and \"1 to 4 key takeaways\" generated per `SocialMediaPost`\n",
    "- Notice that you can nest models. Indeed, `SocialMediaPost.concepts` is a list of `Concept` models.\n",
    "- We use `SkipJsonSchema` on the `youtube_url` field to specify that this shouldn't be generated by the LLM. Instead, we'll manually pass it.\n",
    "\n",
    "Tip: We're adding a `.display()` method to format the text to be more easily human-readable. "
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 1,
   "metadata": {},
   "outputs": [],
   "source": [
    "import textwrap\n",
    "from typing import Union\n",
    "\n",
    "from pydantic import BaseModel, Field\n",
    "from pydantic.json_schema import SkipJsonSchema\n",
    "\n",
    "class Concept(BaseModel):\n",
    "    term: str = Field(description=\"A key term or concept mentioned.\")\n",
    "    definition: str = Field(description=\"A brief definition or explanation of the term.\")\n",
    "    timestamp: float = Field(description=\"Timestamp when the concept is explained.\")\n",
    "\n",
    "    def display(self):\n",
    "        minutes, seconds = divmod(self.timestamp, 60)\n",
    "        return f\"{int(minutes)}:{int(seconds)} - {self.term}: {self.definition}\"\n",
    "\n",
    "\n",
    "class SocialMediaPost(BaseModel):\n",
    "    \"\"\"A social media post about a YouTube video generated its transcript\"\"\"\n",
    "\n",
    "    topic: str = Field(description=\"Main topic discussed.\")\n",
    "    hook: str = Field(description=\"Statement to grab the attention of the reader and announce the topic.\")\n",
    "    body: str = Field(description=\"The body of the social media post. It should be informative and make the reader curious about viewing the video.\")\n",
    "    concepts: list[Concept] = Field(\n",
    "        description=\"Important concepts about Hamilton or Burr mentioned in this post.\",\n",
    "        min_items=1,\n",
    "        max_items=3,\n",
    "    )\n",
    "    key_takeaways: list[str] = Field(\n",
    "        description=\"A list of informative key takeways for the reader.\",\n",
    "        min_items=1,\n",
    "        max_items=4,\n",
    "    )\n",
    "    youtube_url: SkipJsonSchema[Union[str, None]] = None\n",
    "\n",
    "    def display(self) -> str:\n",
    "        formatted_takeways = \" \".join([t for t in self.key_takeaways])\n",
    "        formatted_concepts = \"CONCEPTS\\n\" + \"\\n\".join([c.display() for c in self.concepts])\n",
    "        link = f\"link: {self.youtube_url}\\n\\n\" if self.youtube_url else \"\"\n",
    "\n",
    "        return textwrap.dedent(\n",
    "            f\"\"\"\\\n",
    "            TOPIC: {self.topic}\n",
    "\n",
    "            {self.hook}\n",
    "\n",
    "            {self.body}\n",
    "\n",
    "            {formatted_takeways}\n",
    "\n",
    "            \"\"\"\n",
    "        ) + link + formatted_concepts"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### 2. Write the application logic\n",
    "\n",
    "Instructor is not opiniated about how you write your application; it's only in contact with your LLM client. Here, we write a script in a few lines of code to retrieve a YouTube transcript."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 2,
   "metadata": {},
   "outputs": [],
   "source": [
    "from youtube_transcript_api import YouTubeTranscriptApi\n",
    "\n",
    "# get the video id from a YouTube url\n",
    "youtube_url = \"https://www.youtube.com/watch?v=hqutVJyd3TI\" \n",
    "_, _, video_id = youtube_url.partition(\"?v=\")\n",
    "\n",
    "# get the available YouTube transcript for the video\n",
    "transcript = YouTubeTranscriptApi.get_transcript(video_id)\n",
    "# join the transcript into a single block of text\n",
    "full_transcript = \" \".join([f\"ts={entry['start']} - {entry['text']}\" for entry in transcript])"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### Wrap the LLM client\n",
    "To use Instructor, we need to wrap the OpenAI client, creating a special client. \n",
    "\n",
    "> NOTE: If you have the environment variable `OPENAI_API_KEY` set, the client will be automatically created. Otherwise, you'll need to manually pass the key to `OpenAI(api_key=...)`."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 5,
   "metadata": {},
   "outputs": [],
   "source": [
    "import instructor\n",
    "from openai import OpenAI\n",
    "\n",
    "llm_client = instructor.from_openai(OpenAI())"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "#### Use the LLM client with the `response_model`\n",
    "\n",
    "1. Use the LLM client with `.create` to call the LLM API\n",
    "2. Pass `SocialMediaPost` as the response model, enabling structured outputs.\n",
    "3. The `messages` include the `system` message with the task instruction for the LLM and\n",
    "the `user` message with the input content. "
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 6,
   "metadata": {},
   "outputs": [],
   "source": [
    "response = llm_client.chat.completions.create(\n",
    "    model=\"gpt-4o-mini\",\n",
    "    response_model=SocialMediaPost,\n",
    "    messages=[\n",
    "        {\n",
    "            \"role\": \"system\",\n",
    "            \"content\": \"Analyze the given YouTube transcript and generate a compelling social media post.\",\n",
    "        },\n",
    "        {\"role\": \"user\", \"content\": full_transcript},\n",
    "    ],\n",
    ")"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "`response` will have the type of the provided `response_model`, `SocialMediaPost` in this case. You can use `Model.model_dump()` to get a Python dictionary."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 7,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "<class '__main__.SocialMediaPost'>\n",
      "{'topic': 'Understanding B: A Powerful Tool for Agent Applications', 'hook': '🚀 Debug with ease using B!', 'body': 'Ever faced frustrating issues while building agent applications? Dive into our latest video where we unveil the capabilities of B, a framework designed to enhance observability and debugging. Learn how to fix bugs without restarting your entire agent and explore B’s unique graph-building capabilities for a seamless coding experience. Perfect for developers looking to streamline their coding processes!', 'concepts': [{'term': 'Agent Applications', 'definition': 'Applications that utilize AI agents to perform tasks or make decisions based on input and graphs of actions.', 'timestamp': 108.0}, {'term': 'Graph Building', 'definition': 'Creating a flowchart-like structure of actions and states for better process management in applications.', 'timestamp': 179.0}, {'term': 'Local Tracker', 'definition': 'A feature within B that allows interaction with the application state for tracking and debugging purposes.', 'timestamp': 260.0}], 'key_takeaways': ['B enables easy bug fixing without restarting your agent.', 'Graph-based modeling simplifies the coding process.', 'Integrate local tracking for comprehensive observability in applications.'], 'youtube_url': None}\n"
     ]
    }
   ],
   "source": [
    "print(type(response))\n",
    "print(response.model_dump())"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "We can also use the `.display()` method we've defined!"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 8,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "TOPIC: Understanding B: A Powerful Tool for Agent Applications\n",
      "\n",
      "🚀 Debug with ease using B!\n",
      "\n",
      "Ever faced frustrating issues while building agent applications? Dive into our latest video where we unveil the capabilities of B, a framework designed to enhance observability and debugging. Learn how to fix bugs without restarting your entire agent and explore B’s unique graph-building capabilities for a seamless coding experience. Perfect for developers looking to streamline their coding processes!\n",
      "\n",
      "B enables easy bug fixing without restarting your agent. Graph-based modeling simplifies the coding process. Integrate local tracking for comprehensive observability in applications.\n",
      "\n",
      "CONCEPTS\n",
      "1:48 - Agent Applications: Applications that utilize AI agents to perform tasks or make decisions based on input and graphs of actions.\n",
      "2:59 - Graph Building: Creating a flowchart-like structure of actions and states for better process management in applications.\n",
      "4:20 - Local Tracker: A feature within B that allows interaction with the application state for tracking and debugging purposes.\n"
     ]
    }
   ],
   "source": [
    "print(response.display())"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Burr 101\n",
    "Burr is a tool to build LLM applications, solving many challenges to get to production (monitoring, persistence, streaming, and more). With the concepts of \"state\" and \"action\", you can define complex apps that are easy-to-understand and debug.\n",
    "\n",
    "To show this, we'll rewrite the previous application logic with Burr."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### 1. Define `actions`\n",
    "\n",
    "First, you need to define the different actions your agent can take. This is done by writing Python functions with the `@action` decorator. The decorator must specify the information that can be read from state. Also, the function needs to take a `State` object as first argument and return a `State` object."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 9,
   "metadata": {},
   "outputs": [],
   "source": [
    "from burr.core import State, action\n",
    "\n",
    "\n",
    "@action(reads=[], writes=[\"transcript\"])\n",
    "def get_youtube_transcript(state: State, youtube_url: str) -> State:\n",
    "    \"\"\"Get the official YouTube transcript for a video given it's URL\"\"\"\n",
    "    _, _, video_id = youtube_url.partition(\"?v=\")\n",
    "    \n",
    "    transcript = YouTubeTranscriptApi.get_transcript(video_id, languages=[\"en\"])\n",
    "    full_transcript = \" \".join([f\"ts={entry['start']} - {entry['text']}\" for entry in transcript])\n",
    "\n",
    "    # store the transcript in state\n",
    "    return state.update(transcript=full_transcript, youtube_url=youtube_url)\n",
    "\n",
    "\n",
    "@action(reads=[\"transcript\"], writes=[\"post\"])\n",
    "def generate_post(state: State, llm_client) -> State:\n",
    "    \"\"\"Use the Instructor LLM client to generate `SocialMediaPost` from the YouTube transcript.\"\"\"\n",
    "\n",
    "    # read the transcript from state\n",
    "    transcript = state[\"transcript\"]\n",
    "\n",
    "    response = llm_client.chat.completions.create(\n",
    "        model=\"gpt-4o-mini\",\n",
    "        response_model=SocialMediaPost,\n",
    "        messages=[\n",
    "            {\n",
    "                \"role\": \"system\",\n",
    "                \"content\": \"Analyze the given YouTube transcript and generate a compelling social media post.\",\n",
    "            },\n",
    "            {\"role\": \"user\", \"content\": transcript},\n",
    "        ],\n",
    "    )\n",
    "\n",
    "    # add the youtube_url found in state to the SocialMediaPost\n",
    "    response.youtube_url = state[\"youtube_url\"]\n",
    "\n",
    "    # store the chapters in state\n",
    "    return state.update(post=response)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### 2. Assemble the `Application`\n",
    "\n",
    "To create a Burr agent, we need to assemble the `actions` into an `Application`. This requires specifying the valid `transitions` between actions using tuples of action names `(from, to)` and defining an `entrypoint` from where to begin execution. Then, we can visualize the graph of possible states and actions.\n",
    "\n",
    "Notice that we create the Instructor LLM client *outside* the application and pass it to the `generate_chapters` action via the `.bind()` method. This method follows the same logic as the standard library `functools.partial()`."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 10,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "image/svg+xml": [
       "<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"no\"?>\n",
       "<!DOCTYPE svg PUBLIC \"-//W3C//DTD SVG 1.1//EN\"\n",
       " \"http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd\">\n",
       "<!-- Generated by graphviz version 2.43.0 (0)\n",
       " -->\n",
       "<!-- Title: %3 Pages: 1 -->\n",
       "<svg width=\"200pt\" height=\"174pt\"\n",
       " viewBox=\"0.00 0.00 200.38 174.00\" xmlns=\"http://www.w3.org/2000/svg\" xmlns:xlink=\"http://www.w3.org/1999/xlink\">\n",
       "<g id=\"graph0\" class=\"graph\" transform=\"scale(1 1) rotate(0) translate(4 170)\">\n",
       "<title>%3</title>\n",
       "<polygon fill=\"white\" stroke=\"transparent\" points=\"-4,4 -4,-170 196.38,-170 196.38,4 -4,4\"/>\n",
       "<!-- get_youtube_transcript -->\n",
       "<g id=\"node1\" class=\"node\">\n",
       "<title>get_youtube_transcript</title>\n",
       "<path fill=\"none\" stroke=\"black\" d=\"M175.19,-101C175.19,-101 17.19,-101 17.19,-101 11.19,-101 5.19,-95 5.19,-89 5.19,-89 5.19,-77 5.19,-77 5.19,-71 11.19,-65 17.19,-65 17.19,-65 175.19,-65 175.19,-65 181.19,-65 187.19,-71 187.19,-77 187.19,-77 187.19,-89 187.19,-89 187.19,-95 181.19,-101 175.19,-101\"/>\n",
       "<text text-anchor=\"middle\" x=\"96.19\" y=\"-79.3\" font-family=\"Times,serif\" font-size=\"14.00\">get_youtube_transcript</text>\n",
       "</g>\n",
       "<!-- generate_post -->\n",
       "<g id=\"node3\" class=\"node\">\n",
       "<title>generate_post</title>\n",
       "<path fill=\"none\" stroke=\"black\" d=\"M143.19,-36C143.19,-36 49.19,-36 49.19,-36 43.19,-36 37.19,-30 37.19,-24 37.19,-24 37.19,-12 37.19,-12 37.19,-6 43.19,0 49.19,0 49.19,0 143.19,0 143.19,0 149.19,0 155.19,-6 155.19,-12 155.19,-12 155.19,-24 155.19,-24 155.19,-30 149.19,-36 143.19,-36\"/>\n",
       "<text text-anchor=\"middle\" x=\"96.19\" y=\"-14.3\" font-family=\"Times,serif\" font-size=\"14.00\">generate_post</text>\n",
       "</g>\n",
       "<!-- get_youtube_transcript&#45;&gt;generate_post -->\n",
       "<g id=\"edge2\" class=\"edge\">\n",
       "<title>get_youtube_transcript&#45;&gt;generate_post</title>\n",
       "<path fill=\"none\" stroke=\"black\" d=\"M96.19,-64.78C96.19,-59.09 96.19,-52.61 96.19,-46.34\"/>\n",
       "<polygon fill=\"black\" stroke=\"black\" points=\"99.69,-46.19 96.19,-36.19 92.69,-46.19 99.69,-46.19\"/>\n",
       "</g>\n",
       "<!-- input__youtube_url -->\n",
       "<g id=\"node2\" class=\"node\">\n",
       "<title>input__youtube_url</title>\n",
       "<ellipse fill=\"none\" stroke=\"black\" stroke-dasharray=\"5,2\" cx=\"96.19\" cy=\"-148\" rx=\"96.38\" ry=\"18\"/>\n",
       "<text text-anchor=\"middle\" x=\"96.19\" y=\"-144.3\" font-family=\"Times,serif\" font-size=\"14.00\">input: youtube_url</text>\n",
       "</g>\n",
       "<!-- input__youtube_url&#45;&gt;get_youtube_transcript -->\n",
       "<g id=\"edge1\" class=\"edge\">\n",
       "<title>input__youtube_url&#45;&gt;get_youtube_transcript</title>\n",
       "<path fill=\"none\" stroke=\"black\" d=\"M96.19,-129.78C96.19,-124.09 96.19,-117.61 96.19,-111.34\"/>\n",
       "<polygon fill=\"black\" stroke=\"black\" points=\"99.69,-111.19 96.19,-101.19 92.69,-111.19 99.69,-111.19\"/>\n",
       "</g>\n",
       "</g>\n",
       "</svg>\n"
      ],
      "text/plain": [
       "<graphviz.graphs.Digraph at 0x7f94ead0d270>"
      ]
     },
     "execution_count": 10,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "from burr.core import ApplicationBuilder\n",
    "\n",
    "application = (\n",
    "    ApplicationBuilder()\n",
    "    .with_actions(\n",
    "        get_youtube_transcript,\n",
    "        generate_post.bind(llm_client=llm_client),\n",
    "    )\n",
    "    .with_transitions(\n",
    "        (\"get_youtube_transcript\", \"generate_post\"),\n",
    "    )\n",
    "    .with_entrypoint(\"get_youtube_transcript\")\n",
    "    .build()\n",
    ")\n",
    "application.visualize()"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### 3. Launch the application\n",
    "\n",
    "Using `application.run()` will make our application iterate through actions and state until it hits a `halt` condition. Here, we will simply halt after completing the `generate_post` action. This will return a tuple of (the last action take, the result of the last action, the state of the app). We also need to pass a `youtube_url` since it's a required input to the `get_youtube_transcript` action."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 11,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "TOPIC: Debugging with Burr and Graph States\n",
      "\n",
      "Ever struggled with debugging your AI agents?\n",
      "\n",
      "Dive into the world of Burr with our latest video! Learn how to leverage state tracking and condition-based flowcharts to handle failures and streamline debugging like never before. Discover how you can monitor your agents, and even resume operations from a specific point in their execution—without starting over! 🚀 Whether you’re a developer building complex applications or simply curious about AI functionality, this video will enhance your understanding of agent workflows. Don't miss out!\n",
      "\n",
      "Learn how to handle failures in AI agent applications effectively. Understand the relationship between actions and state in your applications. Discover how to resume operations from a specific state without restarting the entire application.\n",
      "\n",
      "link: https://www.youtube.com/watch?v=hqutVJyd3TI\n",
      "\n",
      "CONCEPTS\n",
      "10:57 - State Tracking: State tracking allows you to monitor the state of your application at different points in time, making debugging easier.\n",
      "1:50 - Flowchart: In this context, a flowchart represents the actions and their dependencies in your agent application as a directed graph.\n",
      "3:36 - Modality: The condition that determines which action to take next in the flow of your agent’s tasks.\n"
     ]
    }
   ],
   "source": [
    "last_action, result, state = application.run(\n",
    "    halt_after=[\"generate_post\"],\n",
    "    inputs={\"youtube_url\": \"https://www.youtube.com/watch?v=hqutVJyd3TI\"},\n",
    ")\n",
    "\n",
    "# print `post` stored in state\n",
    "print(state[\"post\"].display())"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Why create a Burr application?\n",
    "\n",
    "In a few lines of code, you can query an LLM API and can create powerful productivity utilities. However, user-facing features deserve much more scrutiny, which requires tooling and solving complex engineering problems.\n",
    "\n",
    "Building our app with Burr provides several benefits that we'll detail next:\n",
    "- **Observability**: monitor in real-time and log the execution of your `Application` and view it in Burr's web user interface.\n",
    "- **Persistence**: At any point, you can save the application `State`. This allows to create user sessions (e.g., the conversation history menu in ChatGPT), which helps developers investigate bugs and test potential solutions.\n",
    "- **Portability**: your `Application` can run in a notebook, as a script, as a web service, or anywhere Python runs. We'll show how to use Burr with [FastAPI](https://fastapi.tiangolo.com/)."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### 1. Observability\n",
    "\n",
    "Add the clause `.with_tracker(project=...)` to the `ApplicationBuilder()` to track execution. \n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 11,
   "metadata": {},
   "outputs": [],
   "source": [
    "from burr.core import ApplicationBuilder\n",
    "\n",
    "application = (\n",
    "    ApplicationBuilder()\n",
    "    .with_actions(\n",
    "        get_youtube_transcript,\n",
    "        generate_post.bind(llm_client=llm_client),\n",
    "    )\n",
    "    .with_transitions(\n",
    "        (\"get_youtube_transcript\", \"generate_post\"),\n",
    "    )\n",
    "    .with_entrypoint(\"get_youtube_transcript\")\n",
    "    .with_tracker(project=\"youtube-post\")\n",
    "    .build()\n",
    ")"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "last_action, result, state = application.run(\n",
    "    halt_after=[\"generate_post\"],\n",
    "    inputs={\"youtube_url\": \"https://www.youtube.com/watch?v=hqutVJyd3TI\"},\n",
    ")"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Then, you can launch the web UI via the CLI command `burr`"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### 2. Persistence\n",
    "\n",
    "To showcase this feature, we'll add a `rewrite()` action. It sends to the LLM the social media post and a user input to tweak its content. "
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 13,
   "metadata": {},
   "outputs": [],
   "source": [
    "@action(reads=[\"post\"], writes=[\"post\"])\n",
    "def rewrite_post(state: State, llm_client, user_prompt: str):\n",
    "    post = state[\"post\"]\n",
    "\n",
    "    response = llm_client.chat.completions.create(\n",
    "        model=\"gpt-4o-mini\",\n",
    "        response_model=SocialMediaPost,\n",
    "        messages=[\n",
    "            {\n",
    "                \"role\": \"system\",\n",
    "                \"content\": f\"Take the previously generated social media post and modify it according to the following instructions: {user_prompt}\",\n",
    "            },\n",
    "            {\"role\": \"user\", \"content\": post.model_dump_json()},\n",
    "        ],\n",
    "    )\n",
    "\n",
    "    # pass the youtube_url from the previous post version\n",
    "    response.youtube_url = post.youtube_url\n",
    "\n",
    "    return state.update(post=response)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "By adding the transition `(\"rewrite_post\", \"rewrite_post\")`, we are introducing a graph cycle. Observability and persistence becomes particularly valuable to ensure that the LLM doesn't spiral into non-sense. If that's the case, it could be due to the prompts / instructions in the application code, but also user inputs.\n",
    "\n",
    "We also add a `.with_persister()` clause to store our results."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 22,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "image/svg+xml": [
       "<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"no\"?>\n",
       "<!DOCTYPE svg PUBLIC \"-//W3C//DTD SVG 1.1//EN\"\n",
       " \"http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd\">\n",
       "<!-- Generated by graphviz version 2.43.0 (0)\n",
       " -->\n",
       "<!-- Title: %3 Pages: 1 -->\n",
       "<svg width=\"381pt\" height=\"239pt\"\n",
       " viewBox=\"0.00 0.00 381.28 239.00\" xmlns=\"http://www.w3.org/2000/svg\" xmlns:xlink=\"http://www.w3.org/1999/xlink\">\n",
       "<g id=\"graph0\" class=\"graph\" transform=\"scale(1 1) rotate(0) translate(4 235)\">\n",
       "<title>%3</title>\n",
       "<polygon fill=\"white\" stroke=\"transparent\" points=\"-4,4 -4,-235 377.28,-235 377.28,4 -4,4\"/>\n",
       "<!-- get_youtube_transcript -->\n",
       "<g id=\"node1\" class=\"node\">\n",
       "<title>get_youtube_transcript</title>\n",
       "<path fill=\"none\" stroke=\"black\" d=\"M356.09,-166C356.09,-166 198.09,-166 198.09,-166 192.09,-166 186.09,-160 186.09,-154 186.09,-154 186.09,-142 186.09,-142 186.09,-136 192.09,-130 198.09,-130 198.09,-130 356.09,-130 356.09,-130 362.09,-130 368.09,-136 368.09,-142 368.09,-142 368.09,-154 368.09,-154 368.09,-160 362.09,-166 356.09,-166\"/>\n",
       "<text text-anchor=\"middle\" x=\"277.09\" y=\"-144.3\" font-family=\"Times,serif\" font-size=\"14.00\">get_youtube_transcript</text>\n",
       "</g>\n",
       "<!-- generate_post -->\n",
       "<g id=\"node3\" class=\"node\">\n",
       "<title>generate_post</title>\n",
       "<path fill=\"none\" stroke=\"black\" d=\"M324.09,-101C324.09,-101 230.09,-101 230.09,-101 224.09,-101 218.09,-95 218.09,-89 218.09,-89 218.09,-77 218.09,-77 218.09,-71 224.09,-65 230.09,-65 230.09,-65 324.09,-65 324.09,-65 330.09,-65 336.09,-71 336.09,-77 336.09,-77 336.09,-89 336.09,-89 336.09,-95 330.09,-101 324.09,-101\"/>\n",
       "<text text-anchor=\"middle\" x=\"277.09\" y=\"-79.3\" font-family=\"Times,serif\" font-size=\"14.00\">generate_post</text>\n",
       "</g>\n",
       "<!-- get_youtube_transcript&#45;&gt;generate_post -->\n",
       "<g id=\"edge3\" class=\"edge\">\n",
       "<title>get_youtube_transcript&#45;&gt;generate_post</title>\n",
       "<path fill=\"none\" stroke=\"black\" d=\"M277.09,-129.78C277.09,-124.09 277.09,-117.61 277.09,-111.34\"/>\n",
       "<polygon fill=\"black\" stroke=\"black\" points=\"280.59,-111.19 277.09,-101.19 273.59,-111.19 280.59,-111.19\"/>\n",
       "</g>\n",
       "<!-- input__youtube_url -->\n",
       "<g id=\"node2\" class=\"node\">\n",
       "<title>input__youtube_url</title>\n",
       "<ellipse fill=\"none\" stroke=\"black\" stroke-dasharray=\"5,2\" cx=\"277.09\" cy=\"-213\" rx=\"96.38\" ry=\"18\"/>\n",
       "<text text-anchor=\"middle\" x=\"277.09\" y=\"-209.3\" font-family=\"Times,serif\" font-size=\"14.00\">input: youtube_url</text>\n",
       "</g>\n",
       "<!-- input__youtube_url&#45;&gt;get_youtube_transcript -->\n",
       "<g id=\"edge1\" class=\"edge\">\n",
       "<title>input__youtube_url&#45;&gt;get_youtube_transcript</title>\n",
       "<path fill=\"none\" stroke=\"black\" d=\"M277.09,-194.78C277.09,-189.09 277.09,-182.61 277.09,-176.34\"/>\n",
       "<polygon fill=\"black\" stroke=\"black\" points=\"280.59,-176.19 277.09,-166.19 273.59,-176.19 280.59,-176.19\"/>\n",
       "</g>\n",
       "<!-- rewrite_post -->\n",
       "<g id=\"node4\" class=\"node\">\n",
       "<title>rewrite_post</title>\n",
       "<path fill=\"none\" stroke=\"black\" d=\"M229.59,-36C229.59,-36 146.59,-36 146.59,-36 140.59,-36 134.59,-30 134.59,-24 134.59,-24 134.59,-12 134.59,-12 134.59,-6 140.59,0 146.59,0 146.59,0 229.59,0 229.59,0 235.59,0 241.59,-6 241.59,-12 241.59,-12 241.59,-24 241.59,-24 241.59,-30 235.59,-36 229.59,-36\"/>\n",
       "<text text-anchor=\"middle\" x=\"188.09\" y=\"-14.3\" font-family=\"Times,serif\" font-size=\"14.00\">rewrite_post</text>\n",
       "</g>\n",
       "<!-- generate_post&#45;&gt;rewrite_post -->\n",
       "<g id=\"edge4\" class=\"edge\">\n",
       "<title>generate_post&#45;&gt;rewrite_post</title>\n",
       "<path fill=\"none\" stroke=\"black\" d=\"M252.77,-64.78C242.86,-57.77 231.26,-49.56 220.64,-42.04\"/>\n",
       "<polygon fill=\"black\" stroke=\"black\" points=\"222.57,-39.12 212.38,-36.19 218.52,-44.83 222.57,-39.12\"/>\n",
       "</g>\n",
       "<!-- rewrite_post&#45;&gt;rewrite_post -->\n",
       "<g id=\"edge5\" class=\"edge\">\n",
       "<title>rewrite_post&#45;&gt;rewrite_post</title>\n",
       "<path fill=\"none\" stroke=\"black\" d=\"M241.72,-29.47C252.17,-28.13 259.59,-24.31 259.59,-18 259.59,-13.96 256.55,-10.93 251.61,-8.93\"/>\n",
       "<polygon fill=\"black\" stroke=\"black\" points=\"252.27,-5.49 241.72,-6.53 250.62,-12.29 252.27,-5.49\"/>\n",
       "</g>\n",
       "<!-- input__user_prompt -->\n",
       "<g id=\"node5\" class=\"node\">\n",
       "<title>input__user_prompt</title>\n",
       "<ellipse fill=\"none\" stroke=\"black\" stroke-dasharray=\"5,2\" cx=\"100.09\" cy=\"-83\" rx=\"100.18\" ry=\"18\"/>\n",
       "<text text-anchor=\"middle\" x=\"100.09\" y=\"-79.3\" font-family=\"Times,serif\" font-size=\"14.00\">input: user_prompt</text>\n",
       "</g>\n",
       "<!-- input__user_prompt&#45;&gt;rewrite_post -->\n",
       "<g id=\"edge2\" class=\"edge\">\n",
       "<title>input__user_prompt&#45;&gt;rewrite_post</title>\n",
       "<path fill=\"none\" stroke=\"black\" d=\"M123.21,-65.45C133.18,-58.31 145,-49.85 155.81,-42.11\"/>\n",
       "<polygon fill=\"black\" stroke=\"black\" points=\"158.11,-44.77 164.2,-36.1 154.04,-39.08 158.11,-44.77\"/>\n",
       "</g>\n",
       "</g>\n",
       "</svg>\n"
      ],
      "text/plain": [
       "<graphviz.graphs.Digraph at 0x7f94c0175180>"
      ]
     },
     "execution_count": 22,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "from burr.core import ApplicationBuilder\n",
    "from burr.core.persistence import SQLLitePersister\n",
    "\n",
    "persister = SQLLitePersister(db_path=\".burr.db\", table_name=\"burr_state\")\n",
    "persister.initialize()  # this will create the db and table\n",
    "\n",
    "application = (\n",
    "    ApplicationBuilder()\n",
    "    .with_actions(\n",
    "        get_youtube_transcript,\n",
    "        generate_post.bind(llm_client=llm_client),\n",
    "        rewrite_post.bind(llm_client=llm_client),\n",
    "    )\n",
    "    .with_transitions(\n",
    "        (\"get_youtube_transcript\", \"generate_post\"),\n",
    "        (\"generate_post\", \"rewrite_post\"),\n",
    "        (\"rewrite_post\", \"rewrite_post\"),\n",
    "    )\n",
    "    .with_state_persister(persister)\n",
    "    .with_entrypoint(\"get_youtube_transcript\")\n",
    "    .with_tracker(project=\"youtube-post\")\n",
    "    .build()\n",
    ")\n",
    "application.visualize()"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 23,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "LAST ACTION : generate_post: transcript -> post \n",
      "\n",
      "TOPIC: Enhancing AI Agent Applications with B and Hamilton\n",
      "\n",
      "🚀 Dive into the world of AI agents and discover how to streamline your debugging process with B!\n",
      "\n",
      "Are you tired of constant debugging cycles in your AI projects? In our latest video, we explore B, a framework designed to help you monitor and debug agent applications efficiently. Learn how B allows you to visualize your application’s state, manage complex action flows, and even fork your state at any point to tackle bugs without restarting from scratch. Plus, discover its sister framework, Hamilton, perfect for managing data pipelines! Curious to learn how to simplify your debugging and enhance your application development? 🎥 Watch the full video now!\n",
      "\n",
      "Learn to visualize and debug agent applications effectively with B. Understand how to manage states and actions using a graphical approach. Explore the integration of Hamilton for complex data pipeline processes.\n",
      "\n",
      "link: https://www.youtube.com/watch?v=hqutVJyd3TI\n",
      "\n",
      "CONCEPTS\n",
      "0:1 - B Framework: A framework for building and debugging AI agent applications, allowing for state tracking and action visualization.\n",
      "2:25 - State Object: An object that retains the state of an application, enabling actions to read and write within that context.\n",
      "2:0 - Graph Visualization: A depiction of actions and states in a flowchart-like manner to streamline application behavior and debugging.\n"
     ]
    }
   ],
   "source": [
    "# this will run \n",
    "last_action, result, state = application.run(\n",
    "    halt_after=[\"generate_post\"],\n",
    "    inputs={\"youtube_url\": \"https://www.youtube.com/watch?v=hqutVJyd3TI\"},\n",
    ")\n",
    "print(\"LAST ACTION :\", last_action, \"\\n\")\n",
    "print(state[\"post\"].display())"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 24,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "LAST ACTION : rewrite_post: post -> post \n",
      "\n",
      "TOPIC: Enhancing AI Agent Applications with B and Hamilton\n",
      "\n",
      "🚀 Dive into the world of AI agents and discover how to streamline your debugging process with B!\n",
      "\n",
      "Are you encountering challenges with debugging cycles in your AI projects? In our latest video, we explore B, a framework designed for monitoring and debugging agent applications effectively. With B, you can visualize your application’s state, manage complex action flows, and fork your state to tackle bugs without having to restart from scratch. Additionally, we introduce Hamilton, a framework that complements B by simplifying the management of data pipelines. If you are interested in improving your debugging process and application development, we invite you to watch the full video!\n",
      "\n",
      "Learn to visualize and debug agent applications effectively with B. Understand how to manage states and actions using a graphical approach. Explore the integration of Hamilton for complex data pipeline processes.\n",
      "\n",
      "link: https://www.youtube.com/watch?v=hqutVJyd3TI\n",
      "\n",
      "CONCEPTS\n",
      "0:1 - B Framework: A framework for building and debugging AI agent applications, allowing for state tracking and action visualization.\n",
      "2:25 - State Object: An object that retains the state of an application, enabling actions to read and write within that context.\n",
      "2:0 - Graph Visualization: A depiction of actions and states in a flowchart-like manner to streamline application behavior and debugging.\n"
     ]
    }
   ],
   "source": [
    "last_action, result, state = application.step(\n",
    "    inputs={\"user_prompt\": \"Adopt a professional tone that avoids incredible claims. Stay close to the facts, but demonstrate enthusiasm\"},\n",
    ")\n",
    "\n",
    "print(\"LAST ACTION :\", last_action, \"\\n\")\n",
    "print(state[\"post\"].display())"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Then, you can retrieve the store app ids and resume from there. This requires adding a `.initialize_from()` with the `persister`. Also, we're getting the `app_id` from storage via `persister.list_app_ids()`. By passing it to the `.with_identifiers(app_id=...)`, this \"reloaded application\" will logged and tracked in succession of the original app (as you can see in the Burr UI).\n",
    "\n",
    "Below, we're building a new `Application` from the persisted state. When we print the `State`, we're able to retrieve the previously generated (you can see the matching titles)!"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 36,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Enhancing AI Agent Applications with B and Hamilton\n"
     ]
    }
   ],
   "source": [
    "app_id = persister.list_app_ids(\"\")[0]\n",
    "\n",
    "reloaded_app = (\n",
    "    ApplicationBuilder()\n",
    "    .with_actions(\n",
    "        get_youtube_transcript,\n",
    "        generate_post.bind(llm_client=llm_client),\n",
    "        rewrite_post.bind(llm_client=llm_client),\n",
    "    )\n",
    "    .with_transitions(\n",
    "        (\"get_youtube_transcript\", \"generate_post\"),\n",
    "        (\"generate_post\", \"rewrite_post\"),\n",
    "        (\"rewrite_post\", \"rewrite_post\"),\n",
    "    )\n",
    "    .initialize_from(\n",
    "        persister,\n",
    "        resume_at_next_action=True,\n",
    "        default_state={},\n",
    "        default_entrypoint=\"get_youtube_transcript\",\n",
    "    )\n",
    "    .with_state_persister(persister)\n",
    "    .with_identifiers(app_id=app_id)\n",
    "    .with_tracker(project=\"youtube-post\")\n",
    "    .build()\n",
    ")\n",
    "\n",
    "print(reloaded_app.state[\"post\"].topic)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Persistence is great for iterative development such as tuning your [Instructor model with validators and constraints](https://python.useinstructor.com/concepts/reask_validation/), but it's also a powerful tool for building [test cases and guard rails](https://burr.dagworks.io/examples/guardrails/creating_tests/). The CLI command `burr-test-case` can generate a `pytest.fixture` to resume your app from a given state."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### 3. Portability\n",
    "\n",
    "In the GitHub repository, you can find the same Burr `Application` defined in `application.py`, which can be executed via `python application.py`. Also, we provide a boilerplate FastAPI application in `server.py` which imports the `Application` defined in `application.py`"
   ]
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "Python 3 (ipykernel)",
   "language": "python",
   "name": "python3"
  },
  "language_info": {
   "codemirror_mode": {
    "name": "ipython",
    "version": 3
   },
   "file_extension": ".py",
   "mimetype": "text/x-python",
   "name": "python",
   "nbconvert_exporter": "python",
   "pygments_lexer": "ipython3",
   "version": "3.10.9"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 4
}
